﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Threading;
using Enough.Storage;
using System.Net;
using Windows.Storage;

namespace Enough.Http.Cache
{
    public static class CacheExtensions
    {
        private static StorageFolder _storageFolder;

        private static Dictionary<string, CacheInfo> _cacheInfoDictionary;

        private static long _cacheMaxSize = 0;

        private static long _cacheSize = 0;

        private const long _oneYearInSeconds = 31556926;

        private static string _cacheInfoDictionaryFileName = "CacheInfoDictionary";
        private static bool _saveCacheMetaDataAfterEachRetrieval = true;

        public static async Task<HttpResponseMessage> GetCachedAsync(this HttpClient client, string requestUri)
        {
            HttpResponseMessage responseMessage = await retrieveFromCache(requestUri);
            if (responseMessage != null)
            {
                return responseMessage;
            }
            responseMessage = await client.GetAsync(requestUri);
            responseMessage = await checkCacheParameterAsync(responseMessage, requestUri);

            return responseMessage;
        }

        public static async Task<HttpResponseMessage> GetCachedAsync(this HttpClient client, string requestUri, HttpCompletionOption httpCompletionOption)
        {
            HttpResponseMessage responseMessage = await retrieveFromCache(requestUri);
            if (responseMessage != null)
            {
                return responseMessage;
            }
            responseMessage = await client.GetAsync(requestUri, httpCompletionOption);
            responseMessage = await checkCacheParameterAsync(responseMessage, requestUri);

            return responseMessage;
        }

        public static async Task<HttpResponseMessage> GetCachedAsync(this HttpClient client, string requestUri, CancellationToken cancellationToken)
        {
            HttpResponseMessage responseMessage = await retrieveFromCache(requestUri);
            if (responseMessage != null)
            {
                return responseMessage;
            }
            responseMessage = await client.GetAsync(requestUri, cancellationToken);
            responseMessage = await checkCacheParameterAsync(responseMessage, requestUri);

            return responseMessage;
        }

        public static async Task<HttpResponseMessage> GetCachedAsync(this HttpClient client, string requestUri, HttpCompletionOption httpCompletionOption, CancellationToken cancellationToken)
        {
            HttpResponseMessage responseMessage = await retrieveFromCache(requestUri);
            if (responseMessage != null)
            {
                return responseMessage;
            }
            responseMessage = await client.GetAsync(requestUri, httpCompletionOption, cancellationToken);
            responseMessage = await checkCacheParameterAsync(responseMessage, requestUri);
            return responseMessage;
        }

        public static async Task<HttpResponseMessage> GetCachedAsync(this HttpClient client, Uri uri)
        {
            string uriString = uri.ToString();
            HttpResponseMessage responseMessage = await retrieveFromCache(uriString);
            if (responseMessage != null)
            {
                return responseMessage;
            }
            responseMessage = await client.GetAsync(uri);
            responseMessage = await checkCacheParameterAsync(responseMessage, uriString);

            return responseMessage;
        }

        public static async Task<HttpResponseMessage> GetCachedAsync(this HttpClient client, Uri uri, HttpCompletionOption httpCompletionOption)
        {
            string uriString = uri.ToString();
            HttpResponseMessage responseMessage = await retrieveFromCache(uriString);
            if (responseMessage != null)
            {
                return responseMessage;
            }
            responseMessage = await client.GetAsync(uri, httpCompletionOption);
            responseMessage = await checkCacheParameterAsync(responseMessage, uriString);

            return responseMessage;
        }

        public static async Task<HttpResponseMessage> GetCachedAsync(this HttpClient client, Uri uri, CancellationToken cancellationToken)
        {
            string uriString = uri.ToString();
            HttpResponseMessage responseMessage = await retrieveFromCache(uriString);
            if (responseMessage != null)
            {
                return responseMessage;
            }
            responseMessage = await client.GetAsync(uri, cancellationToken);
            responseMessage = await checkCacheParameterAsync(responseMessage, uriString);

            return responseMessage;
        }

        public static async Task<HttpResponseMessage> GetCachedAsync(this HttpClient client, Uri uri, HttpCompletionOption httpCompletionOption, CancellationToken cancellationToken)
        {
            string uriString = uri.ToString();
            HttpResponseMessage responseMessage = await retrieveFromCache(uriString);
            if (responseMessage != null)
            {
                return responseMessage;
            }
            responseMessage = await client.GetAsync(uri, httpCompletionOption, cancellationToken);
            responseMessage = await checkCacheParameterAsync(responseMessage, uriString);

            return responseMessage;
        }

        /// <summary>
        /// Configures the caching configuration
        /// </summary>
        /// <param name="client">the HttpClient</param>
        /// <param name="folder">the optional folder, if not defined the ApplicationData.Current.LocalFolder is used</param>
        /// <param name="cacheSizeInKilobytes">the optional cache size in kilobytes, use 0 to not limit the cache size (this is the default behavior)</param>
        /// <param name="saveCacheMetaDataAfterEachRetrieval">the optional flag if the meta data should be persisted after each retrieval from the cache. If you set this to false you must call HttpClient.SaveCacheMetaData() before exiting your app.</param>
        /// <returns></returns>
        public static async Task ConfigureCacheAsync(this HttpClient client, StorageFolder folder = null, long cacheSizeInKilobytes = 0, bool saveCacheMetaDataAfterEachRetrieval = true)
        {
            _cacheMaxSize = cacheSizeInKilobytes * 1024;
            _saveCacheMetaDataAfterEachRetrieval = saveCacheMetaDataAfterEachRetrieval;
            await createCacheFolder(folder);
            await loadDictionaryAsync();
        }


        /// <summary>
        /// Deletes the associated cache and all its meta data.
        /// </summary>
        /// <param name="client">the HttpClient</param>
        /// <param name="folder">the optional folder, by default the ApplicationData.Current.LocalFolder is assumed</param>
        public static async void ClearCacheAsync(this HttpClient client, StorageFolder folder = null)
        {
            StorageFolder storageFolder = null;
            try
            {
                if (_cacheInfoDictionary != null)
                {
                    _cacheInfoDictionary.Clear();
                }
                if (folder != null)
                {
                    storageFolder = await folder.GetFolderAsync("Cache");
                }
                else if (_storageFolder != null)
                {
                    storageFolder = _storageFolder;
                }
                else
                {
                    storageFolder = await ApplicationData.Current.LocalFolder.GetFolderAsync("Cache");
                }
                await storageFolder.DeleteAsync();
            }
            catch (System.Exception error)
            {
                Debug.WriteLine(error);
            }
        }

        /// <summary>
        /// Saves the meta data about cache usage. 
        /// Call this method before your app exits if you have configured the cache to not be saved automatically.
        /// </summary>
        /// <param name="client">the HttpClient</param>
        /// <returns>this call is awaitable</returns>
        /// <see cref="ConfigureCache"/>
        public static async Task SaveCacheMetaDataAsync(this HttpClient client)
        {
            if (_cacheInfoDictionary != null)
            {
                await saveDictionaryAsync();
            }
        }

        private static async Task createCacheFolder(StorageFolder folder = null)
        {
            if (folder == null)
            {
                folder = ApplicationData.Current.LocalFolder;
            }
            StorageFolder cacheFolder = null;
            try
            {
                cacheFolder = await folder.CreateFolderAsync("Cache");
            }
            catch (System.Exception)
            { }
            cacheFolder = await folder.GetFolderAsync("Cache");
            _storageFolder = cacheFolder;
        }


        private static async Task loadDictionaryAsync()
        {
            if (_storageFolder == null)
            {
                await createCacheFolder();
            }
            try
            {
                _cacheInfoDictionary = await StorageHelper.LoadObjectAsync<Dictionary<string, CacheInfo>>(_cacheInfoDictionaryFileName, _storageFolder);
                // initialize cache size:
                _cacheSize = _cacheInfoDictionary.Values.Sum(t => t.Size);
            }
            catch (System.IO.FileNotFoundException)
            {
                _cacheInfoDictionary = new Dictionary<string, CacheInfo>();
            }
        }

        private static async Task<StoredHttpResponseMessage> retrieveFromCache(string requestUri)
        {
            if (_cacheInfoDictionary.ContainsKey(requestUri))
            {
                CacheInfo cacheInfo = _cacheInfoDictionary[requestUri];
                return await checkCacheValidationAsync(cacheInfo);
            }
            return null;
        }

        private static async Task<StoredHttpResponseMessage> checkCacheValidationAsync(CacheInfo cacheInfo)
        {
            bool loadFromCache = false;
            if (cacheInfo.MaxAge != null)
            {
                double maxAge = cacheInfo.MaxAge.Value.TotalSeconds;

                if (maxAge != 0)
                {
                    TimeSpan time = DateTime.Now - cacheInfo.CreationTime;
                    loadFromCache = (time.TotalSeconds < maxAge || maxAge >= _oneYearInSeconds);
                }
            }

            if (cacheInfo.ExpireDate != null)
            {
                TimeSpan expireTime = (DateTime)cacheInfo.ExpireDate - DateTime.Now;
                loadFromCache = (expireTime.TotalSeconds > 0)
                    || (((DateTime)cacheInfo.ExpireDate - cacheInfo.CreationTime).TotalSeconds >= _oneYearInSeconds);
                
            }
            if (loadFromCache)
            {
                StoredResponse response = await StorageHelper.TryLoadObjectAsync<StoredResponse>(cacheInfo.FileName, _storageFolder);
                cacheInfo.UseCounter++;
                if (_saveCacheMetaDataAfterEachRetrieval)
                {
                    await saveDictionaryAsync();
                }
                return response.ResponseMessage;
            }
            await removeFromCache(cacheInfo);
            return null;
        }

        private static async Task removeFromCache(CacheInfo cacheInfo)
        {
            _cacheInfoDictionary.Remove(cacheInfo.DictionaryKey);
            if (_saveCacheMetaDataAfterEachRetrieval)
            {
                await saveDictionaryAsync();
            }
            await StorageHelper.DeleteObjectAsync<StoredResponse>(fileName: cacheInfo.FileName, folder: _storageFolder);
            _cacheSize -= cacheInfo.Size;
        }

        private static async Task<HttpResponseMessage> checkCacheParameterAsync(HttpResponseMessage responseMessage, string requestUri)
        {
            if (_cacheInfoDictionary == null)
            {
                await loadDictionaryAsync();
            }
            string header = "no-cache";
            if (responseMessage.Headers.CacheControl != null)
            {
                header = responseMessage.Headers.CacheControl.ToString();
            }

            if (!header.Contains("no-cache"))
            {
                long size = (long)responseMessage.Content.Headers.ContentLength;
                if (_cacheMaxSize > 0 && size > _cacheMaxSize) 
                { 
                    return responseMessage; 
                }
                while (isCacheFull(size))
                {
                    await createCacheSpaceAsync(size);
                }
                return await saveResponseMessageAsync(responseMessage, requestUri, size);
            }
            else
            {
                return responseMessage;
            }
        }

        

        private static async Task<HttpResponseMessage> saveResponseMessageAsync(HttpResponseMessage responseMessage, string requestUri, long size)
        {
            string fileName = createFileName(requestUri);

            TimeSpan? maxAge = getMaxAge(responseMessage);
            DateTime? expire = getExpireDate(responseMessage);

            if (maxAge == null && expire == null)
            {
                return responseMessage;
            }
            else
            {
                StoredResponse response = new StoredResponse
                {
                    CreationTime = DateTime.Now,
                    ExpireDate = expire,
                    MaxAge = maxAge,
                    StatusCode = responseMessage.StatusCode,
                    Headers = getDictionary(responseMessage.Headers),
                    Content = await responseMessage.Content.ReadAsByteArrayAsync()
                };

                CacheInfo cacheInfo = new CacheInfo
                {
                    CreationTime = DateTime.Now,
                    ExpireDate = expire,
                    MaxAge = maxAge,
                    FileName = fileName,
                    OriginalUrl = requestUri,
                    Size = size
                };

                await StorageHelper.SaveObjectAsync(response, fileName, _storageFolder);
                _cacheInfoDictionary.Add(requestUri, cacheInfo);
                if (_saveCacheMetaDataAfterEachRetrieval)
                {
                    await saveDictionaryAsync();
                }
                return responseMessage;
            }

        }

        private static async Task saveDictionaryAsync()
        {
            await StorageHelper.SaveObjectAsync(_cacheInfoDictionary, _cacheInfoDictionaryFileName, _storageFolder);
        }

        private static async Task createCacheSpaceAsync(long size)
        {
            int counter = _cacheInfoDictionary.OrderBy(e => e.Value.UseCounter).First().Value.UseCounter;
            IEnumerable<KeyValuePair<string, CacheInfo>> deleteElements = _cacheInfoDictionary.Where(e => e.Value.UseCounter == counter);
            KeyValuePair<string, CacheInfo> first;
            if (deleteElements.Count() == 1)
            {
                first = deleteElements.First();
            }
            else
            {
                first = deleteElements.OrderBy(e => e.Value.CreationTime).First();
            }
            await removeFromCache(first.Value);
        }

        private static bool isCacheFull(long requestSize)
        {
            if (_cacheMaxSize <= 0)
            {
                return false;
            }
            else if (requestSize >= _cacheMaxSize)
            { // the single request is too large
                return false;
            }
            else
            {
                long newCacheSize = _cacheSize + requestSize;
                if (newCacheSize <= _cacheMaxSize)
                {
                    _cacheSize += requestSize;
                    return false;
                }
                else
                {
                    return true;
                }
            }
        }

        private static DateTime? getExpireDate(HttpResponseMessage responseMessage)
        {
            if (responseMessage.Content.Headers.Expires != null)
            {
                return responseMessage.Content.Headers.Expires.Value.DateTime;
            }
            return null;
        }

        private static TimeSpan? getMaxAge(HttpResponseMessage responseMessage)
        {
            if (responseMessage.Headers.CacheControl != null)
            {
                return responseMessage.Headers.CacheControl.MaxAge;
            }
            return null;
        }

        private static string createFileName(string url)
        {
            string uri = url.Replace('&', '_').Replace('?', '_').Replace('=', '_').Replace(';', '_').Replace('/', '_').Replace(':', '_').Replace('.', '_');
            return uri;
        }

        private static Dictionary<string, IEnumerable<string>> getDictionary(HttpResponseHeaders httpResponseHeaders)
        {
            Dictionary<string, IEnumerable<string>> dict = new Dictionary<string, IEnumerable<string>>();
            foreach (var header in httpResponseHeaders)
            {
                dict.Add(header.Key, header.Value);
            }
            return dict;
        }

    }

    [DataContract]
    public class StoredResponse
    {
        [DataMember]
        public DateTime CreationTime { get; set; }

        [DataMember]
        public DateTime? ExpireDate { get; set; }

        [DataMember]
        public TimeSpan? MaxAge { get; set; }

        [DataMember]
        public byte[] Content { get; set; }

        [DataMember]
        public HttpStatusCode StatusCode { get; set; }

        [DataMember]
        public Dictionary<string, IEnumerable<string>> Headers { get; set; }

        [IgnoreDataMember]
        private StoredHttpResponseMessage _responseMessage;
        [IgnoreDataMember]
        public StoredHttpResponseMessage ResponseMessage
        {
            get
            {
                if (_responseMessage == null)
                {
                    _responseMessage = new StoredHttpResponseMessage(this);
                }
                return _responseMessage;
            }
        }
    }

    public class StoredHttpResponseMessage : HttpResponseMessage
    {
        public StoredHttpResponseMessage(StoredResponse storedResponse)
        {
            StatusCode = storedResponse.StatusCode;
            Content = new ByteArrayContent(storedResponse.Content);
            addHeaders(storedResponse.Headers);
        }

        private void addHeaders(Dictionary<string, IEnumerable<string>> storedHeaders)
        {
            foreach (string key in storedHeaders.Keys)
            {
                Headers.Add(key, storedHeaders[key]);
            }
        }
    }

    [DataContract]
    public class CacheInfo
    {
        [DataMember]
        public string OriginalUrl { get; set; }
        [DataMember]
        public string FileName { get; set; }
        [DataMember]
        public DateTime CreationTime { get; set; }
        [DataMember]
        public DateTime? ExpireDate { get; set; }
        [DataMember]
        public TimeSpan? MaxAge { get; set; }
        [DataMember]
        public long Size { get; set; }
        [DataMember]
        public int UseCounter { get; set; }

        [IgnoreDataMember]
        public string DictionaryKey { get { return OriginalUrl; } }
    }
}